#pragma once

/*日志落地模块的实现
    1、抽象落地基类
    2、派生子类 （根据不同落地方向， 进行派生）
    3、使用工厂模式进行创建与表示分离
*/

#include <iostream>
#include <memory>
#include <fstream>
#include "util.hpp"
#include <cassert>
#include <time.h>
#include <unistd.h>
#include <sstream>

namespace mian_yang
{
    class LogSink
    {
    public:
        using ptr = std::shared_ptr<LogSink>;

        /*让用户自行创建落地方向，所以就需要进行虚函数化析构*/
        virtual ~LogSink() {}
        virtual void log(const char *data, size_t len) {}
    };

    /*落地方向:
    1、标准输出。
    2、指定文件。
    3、滚动文件（以大小进行滚动和以时间进行滚动*/
    class StdoutSink : public LogSink
    {
    public:
        /*什么是落地操作， 就是打印到特定地点。
        标准输出：将日志详细写入到标准输出。
        给定一个data，给定一个len，然后进行输出*/
        void log(const char *data, size_t len)
        {
            std::cout.write(data, len);
        }
    };

    class FileSink : public LogSink
    {
    public:
        /*直接打开文件*/
        FileSink(std::string filename)
            : _filename(filename)
        {
            /*创建日志文件所在的目录*/
            mian_yang::util::File::createDirectory(mian_yang::util::File::path(_filename));

            /*创建并打开这个文件 */
            _ofs.open(_filename, std::ios::binary | std::ios::app);

            /*异常处理*/
            assert(_ofs.is_open());
        }

        /*将日志详细的写入到指定文件*/
        void log(const char *data, size_t len)
        {
            _ofs.write(data, len);
        }

    private:
        std::string _filename;
        std::ofstream _ofs; /*文件句柄*/
    };

    class RollSink : public LogSink
    {
    public:
        /*初始化的时候， 将base用来初始化文件路径
        然后设立一个最大文件大小。
        最后将当前文件大小和namecount变成0*/
        RollSink(const std::string basename, size_t max_size)
            : _basename(basename),
              _max_fileSize(max_size),
              _cur_fileSize(0),
              name_count(0)
        {
            std::string filename = createNewFile();                  /*获得目录 + 文件名*/
            util::File::createDirectory(util::File::path(filename)); /*利用目录创建文件*/
            _ofs.open(filename, std::ios::binary | std::ios::app);   /*打开文件*/
            assert(_ofs.is_open());                                  /*检查是否打开文件*/
        }

        void log(const char *data, size_t len)
        {
            if (_cur_fileSize >= _max_fileSize)
            {
                /*获取文件名*/
                std::string filename = createNewFile();
                _ofs.close(); /*关闭旧文件*/

                /*创建日志文件所在的目录*/
                util::File::createDirectory(util::File::path(filename));
                _ofs.open(filename, std::ios::binary | std::ios::app);

                /*异常处理*/
                assert(_ofs.is_open());
                /*创建出文件名后， 将大小标记位置为0*/
                _cur_fileSize = 0;
            }
            /*写入文件， 然后大小标记位加上len*/
            _ofs.write(data, len);
            _cur_fileSize += len;
        }

    private:
        /*进行大小判断，超过指定大小则创建新文件*/
        std::string createNewFile()
        {
            /*获取系统时间， 以时间来构造文件名和扩展名*/
            time_t cur_t = util::Date::GetTime();
            /*利用localtime_r函数将获取到的时间戳
            进行格式化*/
            struct tm l_cur_t;
            localtime_r(&cur_t, &l_cur_t);

            /*初始化文件名*/
            std::string filename = _basename;
            /*构造文件名*/
            std::stringstream ss;
            ss << filename;
            ss << l_cur_t.tm_year + 1900;
            ss << l_cur_t.tm_mon + 1;
            ss << l_cur_t.tm_mday;
            ss << l_cur_t.tm_hour;
            ss << l_cur_t.tm_min;
            ss << l_cur_t.tm_sec;
            /*防止同一秒内创建多个文件，所以使用count计数*/
            ss << "-" << name_count++;
            ss << ".log";
            return ss.str();
        }

    private:
        /*通过基础文件名 + 扩展文件名（以时间生成）组成一个
        实际的当前的输出文件名*/
        std::string _basename; //./logs/base-
        std::ofstream _ofs;    /*文件句柄*/
        size_t _cur_fileSize;  /*当前文件已经写入到的文件大小*/
        size_t _max_fileSize;  /*记录文件大小，当前文件超过了这个大小，就要切换文件*/
        int name_count;        /**/
    };

    /*按照时间进行滚动*/
    /*枚举一下时间间隔*/
    enum class TimeGap
    {
        GAP_SECOND, /*秒间隔*/
        GAP_MINUTE, /*分钟间隔*/
        GAP_HOUR,   /*时间隔*/
        GAP_DAY     /*天间隔*/
    };
    class RollByTimeSink : public LogSink
    {
    public:
        RollByTimeSink(const std::string basename, TimeGap gap)
            : _basename(basename)
        {
            /*同样，的，先获取文件名，打开文件，然后写入数据
            设定时间段的大小*/
            switch (gap)
            {
            case TimeGap::GAP_SECOND:
                _gap_size = 1;
                break;
            case TimeGap::GAP_MINUTE:
                _gap_size = 60;
                break;
            case TimeGap::GAP_HOUR:
                _gap_size = 3600;
                break;
            case TimeGap::GAP_DAY:
                _gap_size = 3600 * 24;
                break;
            }
            /*获取当前是第几个时间段
            如果_gap_size是1，那么就是直接
            拿到当前的时间，以秒来滚动文件，
            否则就是使用_gap_size来进行滚动文件
            拿到当前是第几个_cur_gap*/
            _cur_gap = _gap_size == 1 ? util::Date::GetTime() : util::Date::GetTime() / _gap_size;
            std::string filename = createNewFile();
            /*创建日志文件所在的目录*/
            util::File::createDirectory(util::File::path(filename));
            _ofs.open(filename, std::ios::binary | std::ios::app);
            assert(_ofs.is_open());
        }

        /*将日志消息写入到滚动文件，
        判断当前时间是否是当前文件的时间段，
        不是则切换文件*/

        void log(const char *data, size_t len)
        {
            /*获取当前时间*/
            time_t cur = util::Date::GetTime();
            if (cur / _gap_size != _cur_gap)
            {
                _ofs.close();
                std::string filename = createNewFile();
                /*创建日志文件所在的目录*/
                util::File::createDirectory(util::File::path(filename));
                /*创建并打开这个文件*/
                _ofs.open(filename, std::ios::binary | std::ios::app);
                /*异常处理*/
                assert(_ofs.is_open()); /*检查文件是否打开成功*/
                _cur_gap = _gap_size == 1 ? cur : cur / _gap_size;
            }
            _ofs.write(data, len);
            _ofs.good();
        }

    private:
        /*获取文件名：利用基础文件 + 时间创建一个实际输出的文件*/
        std::string createNewFile()
        {
            /*获取时间戳*/
            time_t cur_t = util::Date::GetTime();
            struct tm l_cur_t; /*格式化时间对象*/
            /*对时间戳进行格式化*/
            localtime_r(&cur_t, &l_cur_t);

            /*创建输出文件*/
            std::string filename = _basename;
            std::stringstream ss;
            ss << filename;
            ss << l_cur_t.tm_year + 1900;
            ss << l_cur_t.tm_mon + 1;
            ss << l_cur_t.tm_mday;
            ss << l_cur_t.tm_hour;
            ss << l_cur_t.tm_min;
            ss << l_cur_t.tm_sec;
            ss << ".log";
            return ss.str(); /*此时ss里面就是
                              一个可输出的实际
                              文件名*/
        }

    private:
        /*通过基础文件名 + 扩展文件名（以时间生成）组成一个实际的
        文件名，然后在Roll里面打开文件*/
        std::string _basename; /*基础文件名，比如/roll-*/
        std::ofstream _ofs;    /*文件句柄*/
        size_t _cur_gap;       /*当前时间段时第几个*/
        size_t _gap_size;      /*时间段的大小*/
    };

    /*使用简单工厂模式， 将对象创建出来*/
    template <class SinkType>
    class SinkFactory
    {
    public:
        template <class... Args>
        static LogSink::ptr create(Args &&...args)
        {
            /*工厂里面有创建落地函数，直接利用
            指定的工厂，创建指定的落地器。
            同时还能可变参数。*/
            /*创建落地器*/
            return std::make_shared<SinkType>(std::forward<Args>(args)...);
        }
    };
}